package com.george.caffeine.dao.redis;

import cn.hutool.core.collection.CollectionUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.beanutils.BeanUtils;
import org.apache.commons.beanutils.ConvertUtils;
import org.apache.commons.beanutils.converters.DateConverter;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.*;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

import static org.springframework.data.redis.core.TimeoutUtils.toSeconds;

/**
 * @Title: RedisDaoImpl.java
 * @Description: Redis缓存接口实现
 * @Author: George
 * @Date: 2022/3/31 10:11
 */
@Component
@Slf4j
public class RedisDaoImpl implements RedisDao{

    @Resource(name = "stringRedisTemplate")
    private RedisTemplate<Object, String> redisTemplate;

    private ValueOperations<Object, String> valueOps;

    private SetOperations<Object, String> setOps;

    private ListOperations<Object, String> listOps;

    private ZSetOperations<Object, String> zSetOps;

    private HashOperations<Object, String, String> hashOps;

    @PostConstruct
    public void init() {
        this.valueOps = redisTemplate.opsForValue();
        this.setOps = redisTemplate.opsForSet();
        this.listOps = redisTemplate.opsForList();
        this.zSetOps = redisTemplate.opsForZSet();
        this.hashOps = redisTemplate.opsForHash();
        ConvertUtils.register(new DateConverter(null), Date.class);
    }


    @Override
    public String get(Object key) {
        return valueOps.get(key);
    }

    @Override
    public <T> T get(Object key, Class<T> type) {
        String value = valueOps.get(key);
        return JSON.parseObject(value, type);
    }

    @Override
    public <T> T get(Object key, TypeReference<T> typeReference) {
        String value = valueOps.get(key);
        return JSON.parseObject(value, typeReference);
    }

    @Override
    public <T> List<T> getList(Object key, Class<T> type) {
        String value = valueOps.get(key);
        return JSON.parseArray(value, type);
    }

    @Override
    public <K, V> Map<K, V> getMap(Object key, TypeReference<V> typeReference) {
        String value = valueOps.get(key);
        return (Map<K, V>) JSON.parseObject(value, typeReference);
    }

    @Override
    public <T> List<T> multiGet(Collection<Object> keys, Class<T> type) {
        if (keys == null || keys.isEmpty()) {
            return Collections.EMPTY_LIST;
        }
        List<String> values = valueOps.multiGet(keys);
        List<T> data = new ArrayList<>();
        for (String value : values) {
            T t = JSON.parseObject(value, type);
            data.add(t);
        }

        return data;
    }

    @Override
    public <T> List multiGet(Collection<Object> keys, TypeReference<T> typeReference) {
        if (keys == null || keys.isEmpty()) {
            return Collections.EMPTY_LIST;
        }

        List<String> values = valueOps.multiGet(keys);
        List<T> data = new ArrayList<>();

        for (String value : values) {
            if (value == null) {
                continue;
            }

            T t = JSON.parseObject(value, typeReference);
            data.add(t);
        }

        return data;
    }

    @Override
    public Boolean existsKey(Object key) {
        return redisTemplate.hasKey(key);
    }

    @Override
    public void delete(Object key) {
        redisTemplate.delete(key);
    }

    @Override
    public Set<Object> keys(Object key) {
        return redisTemplate.keys(key);
    }

    @Override
    public void put(Object key, Object value) {
        valueOps.set(key, JSON.toJSONString(value));
    }

    @Override
    public void put(Object key, Object value, long timeout) {
        put(key, value, timeout, TimeUnit.SECONDS);
    }

    @Override
    public void put(Object key, Object value, long timeout, TimeUnit unit) {
        valueOps.set(key, JSON.toJSONString(value), timeout, unit);
    }

    @Override
    public void multiPut(Map<Object, Object> keyValue) {
        if (keyValue.isEmpty()) {
            return;
        }

        final Map<Object, String> data = new LinkedHashMap<>(keyValue.size());
        for (Map.Entry<Object, Object> entry : keyValue.entrySet()) {
            data.put(entry.getKey(), JSON.toJSONString(entry.getValue()));
        }

        valueOps.multiSet(data);
    }

    @Override
    public Boolean putIfAbsent(Object key, Object value) {
        return valueOps.setIfAbsent(key, JSON.toJSONString(value));
    }

    @Override
    public String getAndSet(Object key, String value) {
        return valueOps.getAndSet(key, value);
    }

    @Override
    public Boolean multiPutIfAbsent(Map<Object, Object> keyValue) {
        if (keyValue.isEmpty()) {
            return true;
        }

        final Map<Object, String> data = new LinkedHashMap<>(keyValue.size());
        for (Map.Entry<Object, Object> entry : keyValue.entrySet()) {
            data.put(entry.getKey(), JSON.toJSONString(entry.getValue()));
        }

        return valueOps.multiSetIfAbsent(data);
    }

    @Override
    public Boolean expire(Object key, long timeout, TimeUnit unit) {
        return redisTemplate.expire(key, timeout, unit);
    }

    @Override
    public Long getExpire(Object key) {
        return redisTemplate.getExpire(key);
    }


    @Override
    public void replace(Object key, Object value) {
        valueOps.set(key, JSON.toJSONString(value));
    }

    @Override
    public void replace(Object key, Object value, long timeout) {
        this.replace(key, value, timeout, TimeUnit.SECONDS);
    }

    @Override
    public void replace(Object key, Object value, long timeout, TimeUnit unit) {
        valueOps.set(key, JSON.toJSONString(value), timeout, unit);
    }

    @Override
    public void evict(Object key) {
        redisTemplate.delete(key);
    }

    @Override
    public long increment(Object key, long offset) {
        return valueOps.increment(key, offset);
    }

    @Override
    public long increment(Object key, String hashKey, long delta) {
        return hashOps.increment(key, hashKey, delta);
    }

    @Override
    public <T> List<T> add(String key, List<T> valueList) {
        for (T value : valueList) {
            setOps.add(key, JSON.toJSONString(value));
        }

        return valueList;
    }

    @Override
    public <T> List<T> members(String key, Class<T> type) {
        List<T> valueList = new ArrayList<>();
        Set<String> valueSet = setOps.members(key);
        for (String value : valueSet) {
            valueList.add(JSON.parseObject(value, type));
        }

        return valueList;
    }

    @Override
    public boolean isMember(Object key, Object o) {
        return setOps.isMember(key, o);
    }

    @Override
    public Long sAdd(Object key, Object o) {
        return setOps.add(key, JSON.toJSONString(o));
    }

    @Override
    public void leftPush(Object key, Object value) {
        listOps.leftPush(key, JSON.toJSONString(value));
    }

    @Override
    public String rightPop(Object key) {
        return listOps.rightPop(key);
    }

    @Override
    public <T> List<T> rightPop(final Object key, final int size, Class<T> type) {
        if (key == null) {
            throw new NullPointerException();
        }
        if (size <= 0) {
            throw new IllegalArgumentException();
        }
        final List<T> result = new ArrayList<>(size);
        RedisSerializer valueSerializer = redisTemplate.getValueSerializer();
        List<byte[]> list = this.redisTemplate.execute(connection -> {
            List<byte[]> arrayList = new ArrayList<>(size);
            RedisSerializer keySerializer = redisTemplate.getKeySerializer();
            for (int i = 0; i < size; i++) {
                byte[] bytes = connection.rPop(keySerializer.serialize(key));
                arrayList.add(bytes);
            }
            return arrayList;
        }, true, true);

        for (byte[] bytes : list) {
            String value = (String) valueSerializer.deserialize(bytes);
            result.add(JSON.parseObject(value, type));
        }

        return result;
    }

    @Override
    public Long length(Object key) {
        return listOps.size(key);
    }

    @Override
    public Long ttl(Object key) {
        return redisTemplate.getExpire(key);
    }

    @Override
    public void addSet(Object key, String value, double score) {
        zSetOps.add(key, value, score);
    }

    @Override
    public void addSet(Object key, Set<ZSetOperations.TypedTuple<String>> tuples) {
        zSetOps.add(key, tuples);
    }

    @Override
    public Long getSetSize(Object key) {
        return zSetOps.size(key);
    }

    @Override
    public Set<String> getSetByRevRange(Object key, long start, long end) {
        return zSetOps.reverseRange(key, start, end);
    }

    @Override
    public Set<String> getSetByRange(Object key, long start, long end) {
        return zSetOps.range(key, start, end);
    }

    @Override
    public void clearSet(Object key) {
        zSetOps.removeRange(key, 0, zSetOps.size(key));
    }

    @Override
    public boolean setIfAbsent(Object key, Object value) {
        return valueOps.setIfAbsent(key, JSON.toJSONString(value));
    }

    @Override
    public <K> void expireByPipe(final Map<K, Long> data) {
        redisTemplate.executePipelined(new RedisCallback<Object>() {
            RedisSerializer redisSerializer = redisTemplate.getKeySerializer();
            byte[] key = null;

            @Override
            public Object doInRedis(RedisConnection connection) throws DataAccessException {
                for (Map.Entry<K, Long> entry : data.entrySet()) {
                    key = redisSerializer.serialize(entry.getKey());
                    connection.expire(key, entry.getValue());
                }

                return null;
            }
        });
    }

    @Override
    public <T> List<T> rightPopByPipe(final Object key, final int size, Class<T> type) {
        if (Objects.isNull(key)) {
            throw new NullPointerException();
        }
        if (size <= 0) {
            throw new IllegalArgumentException();
        }
        final List<T> result = new ArrayList<>(size);
        RedisSerializer keySerializer = redisTemplate.getValueSerializer();
        List<Object> list = redisTemplate.executePipelined((RedisConnection connection) -> {
            for (int i = 0; i < size; i++) {
                connection.lPop(keySerializer.serialize(key));
            }

            return null;
        });

        list.stream().filter(Objects::nonNull).forEach(v -> {
            result.add(JSON.parseObject(v.toString(), type));
        });

        return result;
    }

    @Override
    public <T> void hashMultiPutByPipe(final Map<Object, T> kv) {
        redisTemplate.executePipelined(new RedisCallback<Object>() {
            RedisSerializer rs = redisTemplate.getKeySerializer();
            byte[] key = null;
            byte[] field = null;
            byte[] value = null;

            @Override
            public Object doInRedis(RedisConnection connection) throws DataAccessException {
                for (Map.Entry<Object, T> entry : kv.entrySet()) {
                    key = rs.serialize(entry.getKey());
                    for (Map.Entry<String, String> fieldEt : describe(entry.getValue()).entrySet()) {
                        field = rs.serialize(fieldEt.getKey());
                        value = rs.serialize(fieldEt.getValue());

                        if (value != null) {
                            connection.hSet(key, field, value);
                        }
                    }
                }

                return null;
            }
        });
    }

    @Override
    public <T> List<T> hashMultiGetByPipe(final Collection<Object> keys, final Class<T> type) {
        List<T> result = new ArrayList<>();
        Map<String, ? extends Object> deserializerMap;
        List<Object> maps = redisTemplate.executePipelined(new RedisCallback<Map<Byte[], Byte[]>>() {
            RedisSerializer rs = redisTemplate.getKeySerializer();
            byte[] key = null;

            @Override
            public Map<Byte[], Byte[]> doInRedis(RedisConnection connection) throws DataAccessException {
                for (Object k : keys) {
                    key = rs.serialize(k);
                    connection.hGetAll(key);
                }

                return null;
            }
        });

        for (Object obj : maps) {
            deserializerMap = (Map<String, ? extends Object>) obj;

            if (!deserializerMap.isEmpty()) {
                result.add(populate(type, deserializerMap));
            }
        }

        return result;
    }

    @Override
    public <T, U> List<T> multiGetByPipe(List<U> keys, Class<T> clazz) {
        List<Object> result = redisTemplate.executePipelined((RedisConnection connection) -> {
            RedisSerializer rs = redisTemplate.getKeySerializer();
            keys.forEach(k -> {
                connection.get(rs.serialize(k));
            });

            return null;
        });

        if (Objects.isNull(result)) {
            return null;
        }

        return result.stream()
                .filter(v -> Objects.nonNull(v))
                .map(v -> JSON.parseObject((String) v, clazz))
                .collect(Collectors.toList());
    }

    @Override
    public <K, V> void multiPutByPipe(Map<K, V> m, long timeout, TimeUnit unit) {
        final long rawTimeout = toSeconds(timeout, unit);
        redisTemplate.executePipelined((RedisConnection connection) -> {
            RedisSerializer rs = redisTemplate.getKeySerializer();
            m.forEach((k, v) -> {
                connection.setEx(rs.serialize(k), rawTimeout, rs.serialize(JSON.toJSONString(v)));
            });

            return null;
        });
    }

    /**
     * 把当前java对象把换成map对象
     *
     * @param bean 数据对象
     * @return 对象map
     */
    private <T> Map<String, String> describe(T bean) {
        Map<String, String> kv = null;

        try {
            kv = BeanUtils.describe(bean);
        } catch (Exception e) {
            log.warn(e.getMessage());
            kv = new HashMap<>(16);
        }

        return kv;
    }

    /**
     * 把map换成相应的bean对象
     *
     * @param clazz 数据对象
     * @param kv    待转换的field对象和值
     */
    private <T> T populate(Class<T> clazz, Map<String, ? extends Object> kv) {
        T bean = null;

        try {
            bean = clazz.newInstance();
            BeanUtils.populate(bean, kv);
        } catch (Exception e) {
            log.warn("{}, {}, {}", new Object[]{e.getMessage(), kv, clazz});
        }

        return bean;
    }

    /**
     * 直接拿到RedisConnection连接来使用
     *
     * @param action
     * @return
     */
    @Override
    public <T> T execute(RedisCallback<T> action) {
        try {
            return redisTemplate.execute(action);
        } catch (Exception e) {
            log.info("getAndSet fail:{}", e.toString());
            return null;
        }
    }

    /**
     * 根据key前缀进行删除
     *
     * @param prefix key的前缀
     */
    @Override
    public void deleteByPrefix(String prefix) {
        Set<Object> keys = redisTemplate.keys(prefix + "*");
        if (CollectionUtil.isNotEmpty(keys)) {
            redisTemplate.delete(keys);
        }
    }
}
